import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
from ..registry import BACKBONES


class GCN(nn.Module):
    def __init__(self, in_channels, out_channels, vertex_nums=25, stride=1):
        super(GCN, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=in_channels,
                               out_channels=3 * out_channels,
                               kernel_size=1,
                               stride=1)
        self.conv2 = nn.Conv2d(in_channels=vertex_nums * 3,
                               out_channels=vertex_nums,
                               kernel_size=1)

    def forward(self, x):
        # x --- N,C,T,V
        x = self.conv1(x)  # N,3C,T,V
        x = rearrange(x, 'b (c h) t v -> b c (h v) t', h=3)  # N,C,3,V,T => N,C,3V,T
        x = rearrange(x, 'b c v t -> b v c t')  # N,3V,C,T
        x = self.conv2(x)  # N,V,C,T
        x = rearrange(x, 'b v c t -> b c t v')  # N,C,T,V
        return x


class Block(nn.Module):
    def __init__(self,
                 in_channels,
                 out_channels,
                 vertex_nums=25,
                 temporal_size=9,
                 stride=1,
                 residual=True):
        super(Block, self).__init__()
        self.residual = residual
        self.out_channels = out_channels

        self.bn_res = nn.BatchNorm2d(out_channels)
        self.conv_res = nn.Conv2d(in_channels=in_channels,
                                  out_channels=out_channels,
                                  kernel_size=1,
                                  stride=(stride, 1))
        self.gcn = GCN(in_channels=in_channels,
                       out_channels=out_channels,
                       vertex_nums=vertex_nums)
        self.tcn = nn.Sequential(
            nn.BatchNorm2d(out_channels),
            nn.ReLU(),
            nn.Conv2d(in_channels=out_channels,
                      out_channels=out_channels,
                      kernel_size=(temporal_size, 1),
                      padding=((temporal_size - 1) // 2, 0),
                      stride=(stride, 1)),
            nn.BatchNorm2d(out_channels),
        )

    def forward(self, x):
        if self.residual:
            y = self.conv_res(x)
            y = self.bn_res(y)
        x = self.gcn(x)
        x = self.tcn(x)
        out = x + y if self.residual else x
        out = F.relu(out)
        return out


@BACKBONES.register()
class AGCN(nn.Module):
    """
    AGCN model improves the performance of ST-GCN using
    Adaptive Graph Convolutional Networks.
    Args:
        in_channels: int, channels of vertex coordinate. 2 for (x,y), 3 for (x,y,z). Default 2.
    """

    def __init__(self, in_channels=2, **kwargs):
        super(AGCN, self).__init__()

        # self.data_bn = nn.BatchNorm1d(25 * 2)
        self.agcn = nn.Sequential(
            Block(in_channels=in_channels, out_channels=64, residual=False, **kwargs),
            Block(in_channels=64, out_channels=64, **kwargs),
            Block(in_channels=64, out_channels=64, **kwargs),
            Block(in_channels=64, out_channels=128, **kwargs),
            Block(in_channels=128, out_channels=128, stride=2, **kwargs),
            Block(in_channels=128, out_channels=128, **kwargs),
            Block(in_channels=128, out_channels=128, **kwargs),
            Block(in_channels=128, out_channels=256, stride=2, **kwargs),
            Block(in_channels=256, out_channels=256, **kwargs),
            Block(in_channels=256, out_channels=256, **kwargs)
        )

        self.pool = nn.AdaptiveAvgPool2d(output_size=(1, 1))

    def forward(self, x):
        N, C, T, V, M = x.shape
        x = rearrange(x, 'b c t v m -> (b m) c t v')  # N*M, C, T, V
        x = self.agcn(x)
        x = self.pool(x)  # NM,C,T,V --> NM,C,1,1
        x = rearrange(x, '(b m) c t v -> b m c t v', m=M).mean(axis=1)  # N,C,1,1
        return x

    def init_weights(self):
        #  among self.modules() Show the layer structure of the network
        for m in self.modules():
            #  nn.Conv2d Layer for processing
            if isinstance(m, (nn.Conv2d, nn.Conv1d)):
                #  use  torch.nn.init.xavier_normal  Method for this layer  weight  To initialize
                torch.nn.init.xavier_normal_(m.weight.data)
                #  And judge whether there is bias (bias), If exist , take  bias  Initialize to full  0
                if m.bias is not None:
                    nn.init.constant_(m.bias,0)
            # BatchNorm2d Layer for processing
            elif isinstance(m, (nn.BatchNorm2d, nn.BatchNorm1d)):
                # Initialize with constants
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)
            #  Linear Layer for processing ,Initialize with constants
            elif isinstance(m, nn.Linear):
                #  Initialization of normal distribution , Make the value obey the normal distribution  N(mean, std)
                torch.nn.init.normal_(m.weight.data, 0, 0.01)
                if m.bias is not None:
                    nn.init.constant_(m.bias,0)


if __name__ == '__main__':
    B, C, T, V, M = 2, 2, 300, 25, 1
    x = torch.rand(size=[B, C, T, V, M])
    md = AGCN()
    y = md(x)
    print(y.shape)
